import python

/**
 * An (artificial) expression corresponding to a class definition.
 * It is recommended to use `ClassDef` instead.
 */
class ClassExpr extends ClassExpr_ {
    /** Gets the metaclass expression */
    Expr getMetaClass() {
        if major_version() = 3
        then
            exists(Keyword metacls |
                this.getAKeyword() = metacls and
                metacls.getArg() = "metaclass" and
                result = metacls.getValue()
            )
        else
            exists(Assign a |
                a = this.getInnerScope().getAStmt() and
                a.getATarget().(Name).getId() = "__metaclass__" and
                result = a.getValue()
            )
    }

    /** Gets the nth keyword argument of this class definition. */
    override DictUnpackingOrKeyword getKeyword(int index) {
        result = this.getKeywords().getItem(index)
    }

    /** Gets a keyword argument of this class definition. */
    override DictUnpackingOrKeyword getAKeyword() { result = this.getKeywords().getAnItem() }

    override Expr getASubExpression() {
        result = this.getABase() or
        result = this.getAKeyword().getValue() or
        result = this.getKwargs() or
        result = this.getStarargs()
    }

    Call getADecoratorCall() {
        result.getArg(0) = this or
        result.getArg(0) = this.getADecoratorCall()
    }

    /** Gets a decorator of this function expression */
    Expr getADecorator() { result = this.getADecoratorCall().getFunc() }

    override AstNode getAChildNode() {
        result = this.getASubExpression()
        or
        result = this.getInnerScope()
    }

    /** Gets a tuple (*) argument of this class definition. */
    Expr getStarargs() { result = this.getABase().(Starred).getValue() }

    /** Gets a dictionary (**) argument of this class definition. */
    Expr getKwargs() { result = this.getAKeyword().(DictUnpacking).getValue() }
}

/** A class statement. Note that ClassDef extends Assign as a class definition binds the newly created class */
class ClassDef extends Assign {
    /* syntax: class name(...): ... */
    ClassDef() {
        /* This is an artificial assignment the rhs of which is a (possibly decorated) ClassExpr */
        exists(ClassExpr c | this.getValue() = c or this.getValue() = c.getADecoratorCall())
    }

    override string toString() { result = "ClassDef" }

    /** Gets the class for this statement */
    Class getDefinedClass() {
        exists(ClassExpr c | this.getValue() = c or this.getValue() = c.getADecoratorCall() |
            result = c.getInnerScope()
        )
    }

    override Stmt getLastStatement() { result = this.getDefinedClass().getLastStatement() }
}

/** The scope of a class. This is the scope of all the statements within the class definition */
class Class extends Class_, Scope, AstNode {
    /**
     * Use getADecorator() instead of getDefinition().getADecorator()
     * Use getMetaClass() instead of getDefinition().getMetaClass()
     */
    deprecated ClassExpr getDefinition() { result = this.getParent() }

    /** Gets a defined init method of this class */
    Function getInitMethod() { result.getScope() = this and result.isInitMethod() }

    /** Gets a method defined in this class */
    Function getAMethod() { result.getScope() = this }

    override Location getLocation() { py_scope_location(result, this) }

    /** Gets the scope (module, class or function) in which this class is defined */
    override Scope getEnclosingScope() { result = this.getParent().getScope() }

    /** Use getEnclosingScope() instead */
    override Scope getScope() { result = this.getParent().getScope() }

    override string toString() { result = "Class " + this.getName() }

    /** Gets the statements forming the body of this class */
    override StmtList getBody() { result = Class_.super.getBody() }

    /** Gets the nth statement in the class */
    override Stmt getStmt(int index) { result = Class_.super.getStmt(index) }

    /** Gets a statement in the class */
    override Stmt getAStmt() { result = Class_.super.getAStmt() }

    /** Gets the name used to define this class */
    override string getName() { result = Class_.super.getName() }

    predicate hasSideEffects() { any() }

    /** Whether this is probably a mixin (has 'mixin' or similar in name or docstring) */
    predicate isProbableMixin() {
        (
            this.getName().toLowerCase().matches("%mixin%")
            or
            this.getDocString().getText().toLowerCase().matches("%mixin%")
            or
            this.getDocString().getText().toLowerCase().matches("%mix-in%")
        )
    }

    override AstNode getAChildNode() { result = this.getAStmt() }

    Expr getADecorator() { result = this.getParent().getADecorator() }

    /** Gets the metaclass expression */
    Expr getMetaClass() { result = this.getParent().getMetaClass() }

    /** Gets the ClassObject corresponding to this class */
    ClassObject getClassObject() { result.getOrigin() = this.getParent() }

    /** Gets the nth base of this class definition. */
    Expr getBase(int index) { result = this.getParent().getBase(index) }

    /** Gets a base of this class definition. */
    Expr getABase() { result = this.getParent().getABase() }

    /** Gets the metrics for this class */
    ClassMetrics getMetrics() { result = this }

    /**
     * Gets the qualified name for this class.
     * Should return the same name as the `__qualname__` attribute on classes in Python 3.
     */
    string getQualifiedName() {
        this.getScope() instanceof Module and result = this.getName()
        or
        exists(string enclosing_name |
            enclosing_name = this.getScope().(Function).getQualifiedName()
            or
            enclosing_name = this.getScope().(Class).getQualifiedName()
        |
            result = enclosing_name + "." + this.getName()
        )
    }

    override predicate containsInScope(AstNode inner) { Scope.super.containsInScope(inner) }

    override predicate contains(AstNode inner) { Scope.super.contains(inner) }
}
